Mesh Mosaics¶
A mosaic is a custom collection of perspectives for the same scene. With ipyniivue, you use the set_slice_mosaic_string to define each tile for a lightbox view. Note that the load_meshes() command is asynchronous, so we need to use the on_mesh_loaded() to set the mosaic string after the mesh is available.
This Jupyter notebook mirrors the mesh mosaic web page.
Note boggle atlas will require upgrade to NiiVue 0.66
In [1]:
from pathlib import Path
from ipyniivue import download_dataset
BASE_API_URL = "https://niivue.com/demos/images/"
DATA_FOLDER = Path("images")
download_dataset(
BASE_API_URL,
DATA_FOLDER,
files=[
"lh.pial",
"lh.curv",
"boggle.lh.annot",
],
)
lh.pial already exists. lh.curv already exists. boggle.lh.annot already exists. Dataset downloaded successfully to images.
In [2]:
import math
import ipywidgets as widgets
from IPython.display import display
import ipyniivue
## Initialize Viewer
nv = ipyniivue.NiiVue(
back_color=(1, 1, 1, 1),
)
nv.set_slice_type(ipyniivue.SliceType.RENDER)
nv.opts.is_colorbar = True
nv.opts.text_height = 0.03
nv.opts.tile_margin = 10
## Define Layer Indices
kCurvLayer = 0
kAtlasLayer = 1
kStatLayer = 2
## Load Mesh with Layers
mesh_layers = [
{
"path": DATA_FOLDER / "lh.curv",
"colormap": "gray",
"cal_min": 0.49,
"cal_max": 0.51,
"opacity": 0.5,
},
{
"path": DATA_FOLDER / "boggle.lh.annot",
"opacity": 0.01,
},
{
"path": DATA_FOLDER / "boggle.lh.annot",
"opacity": 0.5,
"use_negative_cmap": True,
},
]
nv.load_meshes(
[
{
"path": DATA_FOLDER / "lh.pial",
"layers": mesh_layers,
},
]
)
## Configure Layers on Load
# Store initial atlas values for statistics layer
initial_atlas_values = [
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
-3, # index 13
0,
0,
0,
0,
0,
0,
0,
-5, # index 21
0,
0,
7, # index 24
0,
0,
4, # index 27
7, # index 28
0,
0,
0,
0,
0,
0,
0,
]
@nv.on_mesh_loaded
def on_mesh_loaded(mesh):
"""Handle event after mesh is loaded and ready."""
# Configure statistics layer
nv.meshes[0].layers[kStatLayer].cal_min = 3
nv.meshes[0].layers[kStatLayer].cal_max = 7
nv.meshes[0].layers[kStatLayer].colormap = "warm"
nv.meshes[0].layers[kStatLayer].colormap_negative = "winter"
nv.meshes[0].layers[kStatLayer].use_negative_cmap = True
nv.meshes[0].layers[kStatLayer].atlas_values = initial_atlas_values
# Configure layer visibility
nv.meshes[0].layers[kCurvLayer].colorbar_visible = False
nv.meshes[0].layers[kAtlasLayer].colorbar_visible = False
nv.meshes[0].layers[kAtlasLayer].show_legend = False
# Set default shader
nv.set_mesh_shader(nv.meshes[0].id, "Rim")
# Set default border (Opaque border = 1.0)
nv.set_mesh_layer_property(nv.meshes[0].id, kAtlasLayer, "outline_border", 1.0)
# Set mosaic string
nv.opts.slice_mosaic_string = "A R 0 R -0 S R 0 R -0 C R 0 R -0"
## Create Interactive Controls
# Binary curvature checkbox
binary_curv_check = widgets.Checkbox(
value=True, description="Binary Curvature", indent=False
)
def on_binary_curv_change(change):
"""Toggle between binary and gradient curvature display."""
if change["new"]:
# Binary mode
cal_min = 0.49
cal_max = 0.51
else:
# Gradient mode
cal_min = 0.35
cal_max = 0.65
nv.meshes[0].layers[kCurvLayer].cal_min = cal_min
nv.set_mesh_layer_property(nv.meshes[0].id, kCurvLayer, "cal_max", cal_max)
binary_curv_check.observe(on_binary_curv_change, names="value")
# Curvature opacity slider
curv_slider = widgets.IntSlider(
value=50, min=0, max=100, description="Curvature", readout=False
)
def on_curv_change(change):
"""Set curve transparency."""
if change["new"] is not None:
nv.set_mesh_layer_property(
mesh_id=nv.meshes[0].id,
layer_index=kCurvLayer,
attribute="opacity",
value=change["new"] * 0.01,
)
curv_slider.observe(on_curv_change, names="value")
# Border dropdown
border_options = [
("Dark border", -0.01),
("Transparent border", 0.01),
("No border", 0.0),
("Opaque border", 1.0),
]
border_dropdown = widgets.Dropdown(
options=border_options,
value=1.0,
description="Border",
)
def on_border_change(change):
"""Set mesh border style."""
nv.set_mesh_layer_property(
nv.meshes[0].id, kAtlasLayer, "outline_border", change["new"]
)
border_dropdown.observe(on_border_change, names="value")
# Atlas opacity slider
atlas_slider = widgets.IntSlider(
value=1, min=0, max=100, description="Atlas", readout=False
)
def on_atlas_change(change):
"""Set atlas transparency."""
if change["new"] is not None:
nv.set_mesh_layer_property(
mesh_id=nv.meshes[0].id,
layer_index=kAtlasLayer,
attribute="opacity",
value=change["new"] * 0.01,
)
atlas_slider.observe(on_atlas_change, names="value")
# Statistics opacity slider
stat_slider = widgets.IntSlider(
value=50, min=0, max=100, description="Statistics", readout=False
)
def on_stat_change(change):
"""Set stat transparency."""
if change["new"] is not None:
nv.set_mesh_layer_property(
mesh_id=nv.meshes[0].id,
layer_index=kStatLayer,
attribute="opacity",
value=change["new"] * 0.01,
)
stat_slider.observe(on_stat_change, names="value")
# Text area for custom statistics configuration
stats_text = widgets.Textarea(
value="""cal_min = 3
cal_max = 7
colormap = 'warm'
colormap_negative = 'winter'
atlas_values = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -3, 0, 0, 0, 0, 0, 0, 0, -5, 0, 0, 7, 0, 0, 4, 7, 0, 0, 0, 0, 0, 0, 0]""", # noqa: E501
description="Stats Config:",
layout=widgets.Layout(width="100%", height="150px"),
)
# Update button
update_stats_btn = widgets.Button(
description="Update Statistics", button_style="primary"
)
def update_statistics(btn):
"""Update statistics layer properties from text input."""
lines = stats_text.value.split("\n")
layer = nv.meshes[0].layers[kStatLayer]
for line in lines:
line = line.strip()
if not line or "=" not in line:
continue
parts = line.split("=")
if len(parts) != 2:
continue
key = parts[0].strip()
raw_value = parts[1].strip()
# Skip comments
if key.startswith("//") or key.startswith("#"):
continue
try:
# Evaluate the value (handles lists, numbers, strings)
value = eval(raw_value)
# Set the property
if hasattr(layer, key):
setattr(layer, key, value)
# Force update
nv.set_mesh_layer_property(nv.meshes[0].id, kStatLayer, "frame_4d", 0)
except Exception as e:
print(f"Error setting {key}: {e}")
update_stats_btn.on_click(update_statistics)
# Mosaic checkbox
mosaic_check = widgets.Checkbox(value=True, description="Mosaic", indent=False)
def on_mosaic_change(change):
"""Toggle mosaic display."""
if change["new"]:
nv.opts.slice_mosaic_string = "A R 0 R -0 S R 0 R -0 C R 0 R -0"
else:
nv.opts.slice_mosaic_string = ""
mosaic_check.observe(on_mosaic_change, names="value")
# Save button
save_btn = widgets.Button(description="Save Bitmap", button_style="success")
def save_screenshot(btn):
"""Save current view as PNG."""
nv.save_scene("ScreenShot.png")
print("Saved as ScreenShot.png")
save_btn.on_click(save_screenshot)
shader_names = nv.mesh_shader_names()
shader_buttons = []
def create_shader_button(name):
"""Create a shader button."""
btn = widgets.Button(description=name, layout=widgets.Layout(width="auto"))
def on_click(b):
nv.set_mesh_shader(nv.meshes[0].id, name)
btn.on_click(on_click)
return btn
shader_buttons = [create_shader_button(name) for name in shader_names]
# Organize shader buttons in a grid
num_cols = 10
num_rows = math.ceil(len(shader_buttons) / num_cols)
shader_grid = []
for i in range(num_rows):
row_buttons = shader_buttons[i * num_cols : (i + 1) * num_cols]
shader_grid.append(widgets.HBox(row_buttons))
shader_box = widgets.VBox([widgets.Label("Shaders:"), widgets.VBox(shader_grid)])
## Display Complete Interface
# Create the complete interface
interface = widgets.VBox(
[
# Top controls row 1
widgets.HBox(
[
binary_curv_check,
curv_slider,
border_dropdown,
atlas_slider,
]
),
# Top controls row 2
widgets.HBox(
[
stat_slider,
mosaic_check,
save_btn,
]
),
# Main viewer
nv,
# Bottom controls
stats_text,
widgets.HBox([update_stats_btn]),
shader_box,
]
)
display(interface)